/*
 * Slightly modified version of the com.ibatis.common.jdbc.ScriptRunner class
 * from the iBATIS Apache project. Only removed dependency on Resource class
 * and a constructor
 */
/**
 * Copyright 2004-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package gu.sql2java;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringReader;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;

import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import gu.sql2java.exception.DaoException;

import static com.google.common.base.Preconditions.checkNotNull;
/**
 * Tool to run database scripts.
 */
public class ScriptRunner {

	/** The Constant DEFAULT_DELIMITER. */
	private static final String DEFAULT_DELIMITER = ";";

	/** The stop on error. */
	private boolean stopOnError;

	/** The auto commit. */
	private boolean autoCommit;

	/** The log writer. */
	private PrintWriter logWriter = new PrintWriter(System.out);

	/** The error log writer. */
	private PrintWriter errorLogWriter = new PrintWriter(System.err);

	/** The delimiter. */
	private String delimiter = DEFAULT_DELIMITER;

	/** The full line delimiter. */
	private boolean fullLineDelimiter = false;

	private boolean clearComment = false;
	
	private String alias = null;
	private final List<Pattern> clearnPatterns = Lists.newArrayList();
	private final List<Pattern> ignorePatterns = Lists.newArrayList();
	private final Map<Pattern,String> replacePatterns = Maps.newHashMap();
	/** all executable SQL statement list */
	private final List<String> executableSqls= Lists.newLinkedList();
	/**
	 * Default constructor.
	 * @param autoCommit
	 *          the auto commit
	 * @param stopOnError
	 *          stop on error
	 */
	public ScriptRunner(boolean autoCommit, boolean stopOnError) {
		this.autoCommit = autoCommit;
		this.stopOnError = stopOnError;
	}

	/**
	 * Sets the delimiter.
	 *
	 * @param delimiter
	 *          the delimiter
	 * @param fullLineDelimiter
	 *          the full line delimiter
	 * @return this instance
	 */
	public ScriptRunner setDelimiter(String delimiter, boolean fullLineDelimiter) {
		this.delimiter = delimiter;
		this.fullLineDelimiter = fullLineDelimiter;
		return this;
	}

	/**
	 * @param clearComment clear all COMMENT statement in SQL script if true
	 * @return this instance
	 */
	public ScriptRunner setClearComment(boolean clearComment){
		this.clearComment = clearComment;
		return this;
	}
	
	/**
	 * set alias name for database connect project
	 * @param alias
	 * @return this instance
	 */
	public ScriptRunner setAlias(String alias) {
		this.alias = alias;
		return this;
	}

	public ScriptRunner addClearRegex(String regex){
		if(!Strings.isNullOrEmpty(regex)){
			this.clearnPatterns.add(Pattern.compile(regex));
		}
		return this;
	}
	public ScriptRunner addClearRegex(Pattern pattern){
		if(null != pattern){
			this.clearnPatterns.add(pattern);
		}
		return this;
	}
	public ScriptRunner addIgnoreRegex(Pattern pattern){
		if(null != pattern){
			this.ignorePatterns.add(pattern);
		}
		return this;
	}
	public ScriptRunner addReplaceRegex(Pattern pattern,String replacement){
		if(null != pattern && null != replacement){
			this.replacePatterns.put(pattern,replacement);
		}
		return this;
	}
	/**
	 * Setter for logWriter property.
	 *
	 * @param logWriter
	 *          - the new value of the logWriter property
	 * @return this instance
	 */
	public ScriptRunner setLogWriter(PrintWriter logWriter) {
		this.logWriter = logWriter;
		return this;
	}

	/**
	 * Setter for errorLogWriter property.
	 *
	 * @param errorLogWriter
	 *          - the new value of the errorLogWriter property
	 * @return this instance
	 */
	public ScriptRunner setErrorLogWriter(PrintWriter errorLogWriter) {
		this.errorLogWriter = errorLogWriter;
		return this;
	}

	/**
	 * Runs an SQL script (read in using the Reader parameter).
	 *
	 * @param reader
	 *          - the source of the script
	 * @throws IOException
	 *           Signals that an I/O exception has occurred.
	 * @throws DaoException
	 *           the wrapped SQL exception
	 */
	public void runScript(Reader reader) throws IOException, DaoException {
		try {
			Connection connection = Managers.managerInstanceOfAlias(alias).getConnection();
			boolean originalAutoCommit = connection.getAutoCommit();
			boolean commit = false;
			try {
				if (originalAutoCommit != this.autoCommit) {
					connection.setAutoCommit(this.autoCommit);
				}
				runScript(connection, checkNotNull(reader,"reader is null"));
				commit = true;
			} finally {
				if(!autoCommit){
					if(commit){
						connection.commit();	
					}else{
						connection.rollback();
					}
				}
				connection.setAutoCommit(originalAutoCommit);
				Managers.managerInstanceOfAlias(alias).releaseConnection(connection);	
			}

		} catch (SQLException e) {
			throw new DaoException(e);
		}catch (Exception e) {
			Throwables.throwIfInstanceOf(e, IOException.class);
			Throwables.throwIfUnchecked(e);
			throw new RuntimeException(e);
		} 
	}
	/**
	 * Runs an SQL script (read in using the String parameter).
	 * @param input
	 *          - the source of the script
	 * @throws IOException
	 * @throws DaoException
	 */
	public void runScript(String input) throws IOException, DaoException {		
		runScript(new StringReader(checkNotNull(input,"input is null")));
	}
	/**
	 * Runs an SQL script (read in using the String parameter).
	 * @param input
	 *          - the source of the script
	 * @throws IOException
	 * @throws DaoException
	 */
	public void runScript(List<String> input) throws IOException, DaoException {		
		runScript(new StringReader(checkNotNull(Joiner.on('\n').join(input),"input is null")));
	}
	/**
	 * Runs an SQL script (read in using the InputStream parameter).
	 * @param input
	 *          - the source of the script
	 * @throws IOException
	 * @throws DaoException
	 */
	public void runScript(InputStream input) throws IOException, DaoException {		
		runScript(new InputStreamReader(checkNotNull(input,"input is null")));
	}
	
	/**
	 * @return all executable SQL statement of last call runScript
	 */
	public List<String> getExecutableSqls() {
		return executableSqls;
	}

	private boolean isIgnore(String sql){
		for(Pattern p : ignorePatterns){
			p.matcher(sql);
			if(p.matcher(sql).matches()){
				println("--" + sql);
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Runs an SQL script (read in using the Reader parameter) using the connection passed in.
	 *
	 * @param conn
	 *          - the connection to use for the script
	 * @param reader
	 *          - the source of the script
	 * @throws IOException
	 *           if there is an error reading from the Reader
	 * @throws SQLException
	 *           if any SQL errors occur
	 */
	private void runScript(Connection conn, Reader reader) throws IOException, SQLException {
		StringBuilder command = null;
		executableSqls.clear();
		// auto close the reader
		try (LineNumberReader lineReader = new LineNumberReader(reader)){
			String line = null;
			while ((line = lineReader.readLine()) != null) {
				if (command == null) {
					command = new StringBuilder();
				}
				String trimmedLine = line.trim();
				if (trimmedLine.startsWith("--")) {
					println(trimmedLine);
				} else if (trimmedLine.startsWith("#")) {
					println(trimmedLine);
				} else if (trimmedLine.length() < 1 || trimmedLine.startsWith("//")) {
					// Do nothing
				} else if (trimmedLine.length() < 1 || trimmedLine.startsWith("--")) {
					// Do nothing
				} else if (!fullLineDelimiter && trimmedLine.endsWith(getDelimiter())
						|| fullLineDelimiter && trimmedLine.equals(getDelimiter())) {
					command.append(line.substring(0, line.lastIndexOf(getDelimiter())));
					command.append(" ");
					Statement statement = conn.createStatement();
					
					String sql = command.toString();
					
					if(isIgnore(sql)){
						command = null;
						continue;
					}
					if(clearComment){
						sql = sql.replaceAll("COMMENT +'[^']*'", "");
					}
					executableSqls.add(sql);
					for(Pattern p : clearnPatterns){
						sql = p.matcher(sql).replaceAll("");
					}
					for(Entry<Pattern, String> p : replacePatterns.entrySet()){
						sql = p.getKey().matcher(sql).replaceAll(p.getValue());
					}
					println(sql);
					boolean hasResults = false;
					if (stopOnError) {
						hasResults = statement.execute(sql);
					} else {
						try {
							statement.execute(sql);
						} catch (SQLException e) {
							e.fillInStackTrace();
							printlnError("Error executing: " + sql);
							printlnError(e);
						}
					}

//					if (autoCommit && !conn.getAutoCommit()) {
//						conn.commit();
//					}

					ResultSet rs = statement.getResultSet();
					if (hasResults && rs != null) {
						ResultSetMetaData md = rs.getMetaData();
						int cols = md.getColumnCount();
						for (int i = 0; i < cols; i++) {
							String name = md.getColumnLabel(i + 1);
							print(name + "\t");
						}
						println("");
						while (rs.next()) {
							for (int i = 0; i < cols; i++) {
								String value = rs.getString(i + 1);
								print(value + "\t");
							}
							println("");
						}
					}

					command = null;
					try {
						statement.close();
					} catch (Exception e) {
						// Ignore to workaround a bug in Jakarta DBCP
					}
					Thread.yield();
				} else {
					command.append(line);
					command.append(" ");
				}
			}
//			if (!autoCommit) {
//				conn.commit();
//			}
		} catch (SQLException e) {
			e.fillInStackTrace();
			printlnError("Error executing: " + command);
			printlnError(e);
			throw e;
		} catch (IOException e) {
			e.fillInStackTrace();
			printlnError("Error executing: " + command);
			printlnError(e);
			throw e;
		} finally {
//			conn.rollback();
			flush();
		}
	}

	/**
	 * Gets the delimiter.
	 *
	 * @return the delimiter
	 */
	private String getDelimiter() {
		return delimiter;
	}

	/**
	 * Prints the.
	 *
	 * @param o
	 *          the o
	 */
	private void print(Object o) {
		if (logWriter != null) {
			System.out.print(o);
		}
	}

	/**
	 * Println.
	 *
	 * @param o
	 *          the o
	 */
	private void println(Object o) {
		if (logWriter != null) {
			logWriter.println(o);
		}
	}

	/**
	 * Println error.
	 *
	 * @param o
	 *          the o
	 */
	private void printlnError(Object o) {
		if (errorLogWriter != null) {
			errorLogWriter.println(o);
		}
	}

	/**
	 * Flush.
	 */
	private void flush() {
		if (logWriter != null) {
			logWriter.flush();
		}
		if (errorLogWriter != null) {
			errorLogWriter.flush();
		}
	}

}
